"""
Configuration-Driven Builder

A builder that executes build steps defined in extension configuration files
rather than hardcoded Python logic.
"""

import os
import subprocess
import shlex
from pathlib import Path
from typing import Dict, List, Optional, Any
import logging

from .base_builder import BaseBuilder, BuildResult
from .config_parser import ExtensionDescriptor, BuildStep

logger = logging.getLogger(__name__)


class ConfigDrivenBuilder(BaseBuilder):
    """
    Builder that executes build steps from configuration descriptors.
    
    This builder reads build instructions from extension configuration files
    and executes them sequentially, providing a way to define build logic
    without writing Python code.
    """
    
    def __init__(self, extension_name: str, descriptor: ExtensionDescriptor):
        """
        Initialize the configuration-driven builder.
        
        Args:
            extension_name: Name of the extension this builder belongs to
            descriptor: Extension descriptor containing build configuration
        """
        super().__init__(extension_name)
        self.descriptor = descriptor
        self._build_artifacts_cache = None
    
    def can_build(self, source_path: Path) -> bool:
        """
        Check if this builder can build the source at the given path.
        
        Uses source patterns from the descriptor to determine compatibility.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            True if builder can handle this source
        """
        if not source_path.exists() or not source_path.is_dir():
            return False
        
        # If no source patterns defined, assume we can build
        if not self.descriptor.source_patterns:
            return True
        
        # Check if any source patterns match
        for pattern in self.descriptor.source_patterns:
            if self._matches_pattern(source_path, pattern):
                logger.debug(f"Source path {source_path} matches pattern: {pattern}")
                return True
        
        logger.debug(f"Source path {source_path} doesn't match any patterns: {self.descriptor.source_patterns}")
        return False
    
    def build(self, source_path: Path, build_config: Optional[Dict[str, Any]] = None,
              clean: bool = False, verbose: bool = False) -> BuildResult:
        """
        Build the solver framework using configured build steps.
        
        Args:
            source_path: Path to source directory
            build_config: Build configuration parameters
            clean: Whether to perform a clean build
            verbose: Whether to enable verbose output
            
        Returns:
            BuildResult indicating success/failure and build artifacts
        """
        logger.info(f"Starting build for {self.extension_name} at {source_path}")
        
        if not self.descriptor.build_steps:
            logger.warning(f"No build steps defined for extension {self.extension_name}")
            return BuildResult(success=True, message="No build steps defined", artifacts=[])
        
        # Prepare environment
        env = self._prepare_environment(source_path, build_config)
        
        # Execute build steps
        artifacts = []
        for i, step in enumerate(self.descriptor.build_steps):
            logger.info(f"Executing build step {i+1}/{len(self.descriptor.build_steps)}: {step.name}")
            
            try:
                result = self._execute_build_step(step, source_path, env, verbose)
                if not result.success:
                    if step.fail_fast:
                        logger.error(f"Build step '{step.name}' failed: {result.message}")
                        return BuildResult(
                            success=False,
                            message=f"Build failed at step '{step.name}': {result.message}",
                            artifacts=artifacts
                        )
                    else:
                        logger.warning(f"Build step '{step.name}' failed but continuing: {result.message}")
                
                # Collect any artifacts from this step
                artifacts.extend(result.artifacts)
                
            except Exception as e:
                logger.error(f"Exception in build step '{step.name}': {e}")
                if step.fail_fast:
                    return BuildResult(
                        success=False,
                        message=f"Build failed at step '{step.name}': {str(e)}",
                        artifacts=artifacts
                    )
        
        # Add configured artifacts
        configured_artifacts = self.get_build_artifacts(source_path)
        artifacts.extend(configured_artifacts)
        
        # Clear artifacts cache to force refresh
        self._build_artifacts_cache = None
        
        logger.info(f"Build completed successfully for {self.extension_name}")
        return BuildResult(
            success=True,
            message=f"Build completed with {len(self.descriptor.build_steps)} steps",
            artifacts=list(set(artifacts))  # Remove duplicates
        )
    
    def get_build_artifacts(self, source_path: Path) -> List[str]:
        """
        Get list of build artifacts from configuration.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            List of artifact paths relative to source_path
        """
        if self._build_artifacts_cache is not None:
            return self._build_artifacts_cache
        
        artifacts = []
        
        # Add configured artifacts
        for artifact_pattern in self.descriptor.build_artifacts:
            # Expand environment variables and resolve paths
            expanded_pattern = os.path.expandvars(artifact_pattern)
            artifact_path = source_path / expanded_pattern
            
            if artifact_path.exists():
                # Make path relative to source_path
                try:
                    relative_path = artifact_path.relative_to(source_path)
                    artifacts.append(str(relative_path))
                except ValueError:
                    # Path is not relative to source_path, use absolute
                    artifacts.append(str(artifact_path))
            else:
                logger.debug(f"Artifact not found: {artifact_path}")
        
        self._build_artifacts_cache = artifacts
        return artifacts
    
    def validate_dependencies(self) -> List[str]:
        """
        Validate that build dependencies are available.
        
        Returns:
            List of error messages (empty if valid)
        """
        errors = []
        
        # Check if required commands are available
        commands_to_check = set()
        for step in self.descriptor.build_steps:
            # Extract the command (first word) from the command string
            command_parts = shlex.split(step.command)
            if command_parts:
                commands_to_check.add(command_parts[0])
        
        for command in commands_to_check:
            if not self._command_exists(command):
                errors.append(f"Required command '{command}' not found in PATH")
        
        return errors
    
    def get_build_info(self, source_path: Path) -> Dict[str, Any]:
        """
        Get information about the build system.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            Dictionary with build system information
        """
        info = super().get_build_info(source_path)
        info.update({
            'descriptor_name': self.descriptor.name,
            'descriptor_version': self.descriptor.version,
            'build_steps_count': len(self.descriptor.build_steps),
            'build_artifacts_count': len(self.descriptor.build_artifacts),
            'supported_frameworks': self.descriptor.supported_frameworks,
        })
        return info
    
    def clean(self, source_path: Path) -> BuildResult:
        """
        Clean build artifacts.
        
        Args:
            source_path: Path to source directory
            
        Returns:
            BuildResult indicating success/failure
        """
        logger.info(f"Cleaning build artifacts for {self.extension_name}")
        
        removed_count = 0
        errors = []
        
        # Remove configured artifacts
        for artifact_pattern in self.descriptor.build_artifacts:
            try:
                expanded_pattern = os.path.expandvars(artifact_pattern)
                artifact_path = source_path / expanded_pattern
                
                if artifact_path.exists():
                    if artifact_path.is_dir():
                        import shutil
                        shutil.rmtree(artifact_path)
                    else:
                        artifact_path.unlink()
                    removed_count += 1
                    logger.debug(f"Removed artifact: {artifact_path}")
                
            except Exception as e:
                error_msg = f"Failed to remove artifact {artifact_pattern}: {e}"
                logger.warning(error_msg)
                errors.append(error_msg)
        
        # Clear artifacts cache
        self._build_artifacts_cache = None
        
        if errors:
            return BuildResult(
                success=False,
                message=f"Clean partially failed: {'; '.join(errors)}",
                artifacts=[]
            )
        else:
            return BuildResult(
                success=True,
                message=f"Cleaned {removed_count} artifacts",
                artifacts=[]
            )
    
    def get_default_config(self) -> Dict[str, Any]:
        """
        Get default build configuration.
        
        Returns:
            Default configuration dictionary
        """
        return {
            'environment': self.descriptor.build_environment.copy(),
            'parallel_jobs': os.cpu_count() or 1,
            'verbose': False,
        }
    
    def _prepare_environment(self, source_path: Path, build_config: Optional[Dict[str, Any]]) -> Dict[str, str]:
        """Prepare environment variables for build execution."""
        env = os.environ.copy()
        
        # Add descriptor environment
        env.update(self.descriptor.build_environment)
        
        # Add build config environment if provided
        if build_config and 'environment' in build_config:
            env.update(build_config['environment'])
        
        # Set common variables
        env['GSM_SOURCE_PATH'] = str(source_path)
        env['GSM_EXTENSION_NAME'] = self.extension_name
        
        return env
    
    def _execute_build_step(self, step: BuildStep, source_path: Path, 
                           base_env: Dict[str, str], verbose: bool) -> BuildResult:
        """Execute a single build step."""
        
        # Prepare environment for this step
        env = base_env.copy()
        if step.environment:
            env.update(step.environment)
        
        # Determine working directory
        if step.working_dir:
            working_dir = Path(os.path.expandvars(step.working_dir))
            if not working_dir.is_absolute():
                working_dir = source_path / working_dir
        else:
            working_dir = source_path
        
        # Check conditions if any
        if step.conditions and not self._check_conditions(step.conditions, env):
            logger.info(f"Skipping build step '{step.name}' due to unmet conditions")
            return BuildResult(success=True, message="Step skipped due to conditions")
        
        logger.debug(f"Executing command: {step.command}")
        logger.debug(f"Working directory: {working_dir}")
        
        try:
            # Execute the command
            process = subprocess.run(
                step.command,
                shell=True,
                cwd=working_dir,
                env=env,
                capture_output=True,
                text=True,
                timeout=step.timeout
            )
            
            if verbose or process.returncode != 0:
                if process.stdout:
                    logger.info(f"Step '{step.name}' stdout: {process.stdout}")
                if process.stderr:
                    logger.info(f"Step '{step.name}' stderr: {process.stderr}")
            
            if process.returncode == 0:
                return BuildResult(success=True, message=f"Step '{step.name}' completed")
            else:
                return BuildResult(
                    success=False,
                    message=f"Step '{step.name}' failed with exit code {process.returncode}: {process.stderr}"
                )
        
        except subprocess.TimeoutExpired:
            return BuildResult(
                success=False,
                message=f"Step '{step.name}' timed out after {step.timeout} seconds"
            )
        except Exception as e:
            return BuildResult(
                success=False,
                message=f"Step '{step.name}' failed with exception: {str(e)}"
            )
    
    def _check_conditions(self, conditions: Dict[str, Any], env: Dict[str, str]) -> bool:
        """Check if step conditions are met."""
        for condition_type, condition_value in conditions.items():
            if condition_type == 'env_var_set':
                if condition_value not in env:
                    return False
            elif condition_type == 'env_var_equals':
                var_name, expected_value = condition_value.split('=', 1)
                if env.get(var_name) != expected_value:
                    return False
            elif condition_type == 'file_exists':
                if not Path(os.path.expandvars(condition_value)).exists():
                    return False
            elif condition_type == 'platform':
                import platform
                if platform.system().lower() != condition_value.lower():
                    return False
            else:
                logger.warning(f"Unknown condition type: {condition_type}")
                return False
        
        return True
    
    def _matches_pattern(self, source_path: Path, pattern: str) -> bool:
        """Check if source path matches the given pattern."""
        import fnmatch
        
        # Check if pattern matches directory name
        if fnmatch.fnmatch(source_path.name, pattern):
            return True
        
        # Check if pattern matches any file in the directory
        for file_path in source_path.rglob('*'):
            if fnmatch.fnmatch(file_path.name, pattern):
                return True
            # Also check relative path from source
            try:
                rel_path = file_path.relative_to(source_path)
                if fnmatch.fnmatch(str(rel_path), pattern):
                    return True
            except ValueError:
                continue
        
        return False
    
    def _command_exists(self, command: str) -> bool:
        """Check if a command exists in the PATH."""
        import shutil
        return shutil.which(command) is not None